基于Spring Boot,Security和JWB的REST接口的无状态认证

#Stateless Spring Security Part 2: Stateless Authentication
Posted on October
6, 2014
by Robbert
van Waveren

This second part of the Stateless Spring Security series is about exploring means of authentication in a stateless way. If you missed the first part about CSRF you can find it here.
So when talking about Authentication, its all about having the client identify itself to the server in a verifiable manner. Typically this start with the server providing the client with a challenge, like a request to fill in a username / password. Today I
want to focus on what happens after passing such initial (manual) challenge and how to deal with automatic re-authentication of futher HTTP requests.

#Common approaches

###SESSION COOKIE BASED
The most common approach we probably all know is to use a server generated secret token (Session key) in the form of a JSESSIONID cookie. Initial setup for this is near nothing these days perhaps making you forget you have a choice to make here in the first
place. Even without further using this “Session key” to store any other state “in the session”, the key itself is in fact state as
well. I.e. without a shared and persistent storage of these keys, no successful authentication will survive a server reboot or requests being load balanced to another server.

###OAUTH2 / API KEYS
Whenever talking about REST APIs and Security; OAuth2 and other types of API keys are mentioned. Basically they involve sending custom tokens/keys within the HTTP Authorization header. When used properly both relieve clients from dealing with Cookies using
the header instead. This solves CSRF vulnerabilities and other Cookie related issues. One thing they do not solve however is the need for the server to check the presented authentication keys, pretty much demanding some persistent and maintainable shared storage
for linking the keys to users/authorizations.

#Stateless approaches

###1. HTTP BASIC AUTH
The oldest and most crude way of dealing with authentication. Simply have the user send its username/password with every request. This probably sounds horrible, but considering any of the approaches mentioned above also send secret keys over the wire, this
isn’t really all that less secure at all. Its mainly the user experience and flexibility that makes the other approaches a better choice.

###2. SERVER SIGNED TOKENS
A neat little trick to dealing with state across requests in a stateless way is to have the server “sign” it. It can then be transported back and forth between the client/server each request with the guarantee that it is not tampered with. This way any user
identification data can be shared in plain-text, adding a special signing hash to it. Considering it is signed, the server can simply validate if the signing hash still matches the received content, without needing to hold any server-side state.
The common standard that can be used for this is JSON
Web Tokens
(JWT) which is still in draft. For this blog post I’d like to get down and dirty though, skipping full compliance and the scream for using a library that comes with it. Picking just what we actually need from it. (Leaving out the header/variable
hash algoritms and url-safe base64 encoding)


#Implementation
As mentioned we’re going to roll our own implementation, using Spring Security and Spring Boot to plug it all together. Without any library or fancy API obfuscating what’s really happening on the token level. The token is going to look like this in pseudo-code:
12content=toJSON(user_details)token=BASE64(content)+”.”+BASE64(HMAC(content))The dot in the token serves as a separator, so each part can be identified and decoded separately as the dot character is not part of any base64 encoded string. The HMAC stands for a Hash-based Message Authentication Code, which is basically a hash made from any data using a predefined secret key.
In actual Java the generation of the token looks a lot like the pseudo-code:
123456789publicStringcreateTokenForUser(Useruser){byte[]userBytes=toJSON(user);byte[]hash=createHmac(userBytes);finalStringBuildersb=newStringBuilder(170);sb.append(toBase64(userBytes));sb.append(SEPARATOR);sb.append(toBase64(hash));returnsb.toString();}The relevant User properties used in the JSON are id, username, expires and roles, but could be anything you want really.
I marked the “password” property of the User object to be ignored during jackson JSON serialization so it does not become part of the token:
1234@JsonIgnorepublicStringgetPassword(){returnpassword;}For real worlds scenarios you probably just want to use a dedicated object for this.
The decoding of the token is a bit more complex with some input validation to prevent/catch parsing errors due to tampering with the token:
1234567891011121314151617181920publicUserparseUserFromToken(Stringtoken){finalString[]parts=token.split(SEPARATOR_SPLITTER);if(parts.length==2&&parts[0].length()>0&&parts[1].length()>0){try{finalbyte[]userBytes=fromBase64(parts[0]);finalbyte[]hash=fromBase64(parts[1]); booleanvalidHash=Arrays.equals(createHmac(userBytes),hash);if(validHash){finalUseruser=fromJSON(userBytes);if(newDate().getTime()<user.getExpires()){returnuser;}}}catch(IllegalArgumentExceptione){//log
tampering attempt here}}returnnull;}It essentially validates if the provided hash is the same as a fresh computed hash of the content.Because the createHmac method
uses an undisclosed secret key internally to compute the hash, no client will be able to tamper with the content and provide a hash that is the same as the one the server will produce. Only after passing this test the provided data will be interpreted
as JSON representing a User object.
Zooming in on the Hmac part, lets see the exact Java involved. First it must be initialized with a secret key, which I do as part of TokenHandler’s constructor:
123456789101112131415…privatestaticfinalStringHMAC_ALGO=”HmacSHA256”; privatefinalMac hmac; publicTokenHandler(byte[]secretKey){try{hmac=Mac.getInstance(HMAC_ALGO);hmac.init(newSecretKeySpec(secretKey,HMAC_ALGO));}catch(NoSuchAlgorithmException|InvalidKeyExceptione){thrownewIllegalStateException(“failed to initialize HMAC: “+e.getMessage(),e);}}…After initialization it can be (re-)used, using a single method call! (doFinal’s JavaDoc reads “Processes the given array of bytes and finishes the MAC operation. A call to this method resets this Mac object to the state it was in when previously initialized via a call to init(Key) or init(Key, AlgorithmParameterSpec)…”)
1234//
synchronized to guard internal hmac objectprivatesynchronizedbyte[]createHmac(byte[]content){returnhmac.doFinal(content);}I used some crude synchronization here, to prevent conflicts when used within a Spring Singleton Service. The actual method is very fast (~0.01ms) so it shouldn’t cause a problem unless your going for 10k+ requests per seconds per server.
Speaking of the Service, lets work our way up to a fully working token-based authentication service:
123456789101112131415161718192021222324252627282930@ServicepublicclassTokenAuthenticationService{ privatestaticfinalStringAUTH_HEADER_NAME=”X-AUTH-TOKEN”;privatestaticfinallongTEN_DAYS=100060602410; privatefinalTokenHandler tokenHandler; @AutowiredpublicTokenAuthenticationService(@Value(“${token.secret}”)Stringsecret){tokenHandler=newTokenHandler(DatatypeConverter.parseBase64Binary(secret));} publicvoidaddAuthentication(HttpServletResponse response,UserAuthentication authentication){finalUser user=authentication.getDetails();user.setExpires(System.currentTimeMillis()+TEN_DAYS);response.addHeader(AUTH_HEADER_NAME,tokenHandler.createTokenForUser(user));} publicAuthentication getAuthentication(HttpServletRequest request){finalStringtoken=request.getHeader(AUTH_HEADER_NAME);if(token!=null){finalUser user=tokenHandler.parseUserFromToken(token);if(user!=null){returnnewUserAuthentication(user);}}returnnull;}}Pretty straight-forward, initializing a private TokenHandler to do the heavy lifting. It provides methods for adding and reading the custom HTTP token header. As you can see it does not use any (database driven) UserDetailsService to lookup the user details. All details required to let Spring Security handle further authorization checks are provided by means of the token.
Finally we can now plug-in all of this into Spring Security adding two custom filters in the Security configuration:
12345678910111213141516171819…@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http…//
custom JSON based authentication by POST of //
{“username”:”“,”password”:”“} //
which sets the token header upon authentication.addFilterBefore(newStatelessLoginFilter(“/api/login”,…),UsernamePasswordAuthenticationFilter.class) //
custom Token based authentication based on //
the header previously given to the client.addFilterBefore(newStatelessAuthenticationFilter(…),UsernamePasswordAuthenticationFilter.class);}…The StatelessLoginFilter adds the token upon successful authentication:
12345678910111213141516…@OverrideprotectedvoidsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authentication)throwsIOException,ServletException{ // Lookup the complete User object from the database and create an Authentication for itfinalUser authenticatedUser=userDetailsService.loadUserByUsername(authentication.getName());finalUserAuthentication userAuthentication=newUserAuthentication(authenticatedUser); // Add the custom token as HTTP header to the responsetokenAuthenticationService.addAuthentication(response,userAuthentication); // Add the authentication to the Security contextSecurityContextHolder.getContext().setAuthentication(userAuthentication);}…the StatelessAuthenticationFilter simply sets the authentication based upon the header:
12345678910…@OverridepublicvoiddoFilter(ServletRequestreq,ServletResponseres,FilterChainchain)throwsIOException,ServletException{ SecurityContextHolder.getContext().setAuthentication(tokenAuthenticationService.getAuthentication((HttpServletRequest)req));chain.doFilter(req,res);//
always continue}…Note that unlike most Spring Security related filters, I choose to continue down the filter chain regardless of successful authentication. I wanted to support triggering Spring’s AnonymousAuthenticationFilter to support anonymous authentication. The big difference
here being that the filter is not configured to map to any url specifically meant for authentication, so not providing the header isn’t really a fault.


###CLIENT-SIDE IMPLEMENTATION
Client-side implementation is again pretty straight-forward. Again I’m keeping it minimalistic to prevent the authentication bit being lost in AngularJS details. If you’re looking for an AngularJS JWT example more thoroughly integrated with routes you should
take a look here.
I borrowed some of the interceptor logic from it.
Logging in, is simply a matter of storing the token (in localStorage):
1234567$scope.login=function(){varcredentials={username:$scope.username,password:$scope.password};$http.post(‘/api/login’,credentials).success(function(result,status,headers){$scope.authenticated=true;TokenStorage.store(headers(‘X-AUTH-TOKEN’));}); };Logging out is even simpler (no call to the server necessary):
12345$scope.logout=function(){//
Just clear the local storageTokenStorage.clear();$scope.authenticated=false;};To check if a user is “already logged in” ng-init=”init()” works nicely:
12345678$scope.init=function(){$http.get(‘/api/users/current’).success(function(user){if(user.username!==’anonymousUser’){$scope.authenticated=true;$scope.username=user.username;}});};I choose to use an anonymously reachable endpoint to prevent triggering 401/403’s. You could also decode the token itself and check the expiration time, trusting the local client time to be accurate enough.
Finally in order to automate the process of adding the header a simple interceptor much like in last blog entry does nicely:
12345678910111213141516171819factory(‘TokenAuthInterceptor’,function($q,TokenStorage){return{request:function(config){varauthToken=TokenStorage.retrieve();if(authToken){config.headers[‘X-AUTH-TOKEN’]=authToken;}returnconfig;},responseError:function(error){if(error.status===401||error.status===403){TokenStorage.clear();}return$q.reject(error);}};}).config(function($httpProvider){$httpProvider.interceptors.push(‘TokenAuthInterceptor’);});It also takes care of automatically clearing the token after receiving an HTTP 401 or 403, assuming the client isn’t going to allow calls to areas that need higher privileges.

###TOKENSTORAGE
The TokenStorage is just a wrapper service over localStorage which I’ll not bother you with. Putting the token in the localStorage protects it from being read by script outside the origin of the script that saved it, just like cookies. However because the token
is not an actual Cookie, no browser can be instructed add it to requests automatically. This is essential as it completely prevents any form of CSRF attacks. Thus saving you from having to implement any (Stateless) CSRF protection mentioned in my previous
blog.


You can find a complete working example with some nice extras at github
Make sure you have gradle 2.0 installed and simply run it using “gradle build” followed by a “gradle run”. If you want to play with it in your IDE like Eclipse, go with “gradle eclipse” and just import and run it from within your IDE (no server needed).

This entry was posted in AngularJS, Coding, FE, Front
End
, Java, REST, Security by Robbert
van Waveren
. Bookmark the permalink.

Contents
,